package mazeAssignment;

import java.awt.BorderLayout;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.GridLayout;
import java.awt.Insets;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.BorderFactory;
import javax.swing.BoxLayout;
import javax.swing.JApplet;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JProgressBar;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;
import javax.swing.SwingWorker;

/**
 * If a MazeType, Build or Solve implementation is added only the enum's have to be changed.
 * @author Hekman
 *
 */
public class Benchmark implements ActionListener, PropertyChangeListener {
	
	private static final int FRAME_WIDTH = 900;
	private static final int FRAME_HEIGHT = 600;
	
	private static final int OPTIONS_PANEL_HEIGHT = 260;
	private static final int START_PANEL_HEIGHT = 30;
	private static final int INSET_SIZE = 1;
	
	
	private static final int DEFAULT_MAZE_X_SIZE = 40;
	private static final int DEFAULT_MAZE_Y_SIZE = 40;
	private static final int DEFAULT_RUNS = 5;
	
	
	private Container mTopLevelContainer;
	
	private JPanel startAndProgressPanel;
	
	private JTextField txtMazeWidth;
	private JTextField txtMazeHeight;
	private JTextField txtNumberOfRuns;
	private JButton btnStart;
	private JProgressBar progressBar;
	
	private JCheckBox[] ckbxMazeType;
	private JCheckBox[] ckbxBuild;
	private JCheckBox[] ckbxSolve;
	
	private JTextArea txtLog;
	
	public Benchmark()
	{
		SwingUtilities.invokeLater(
				new Runnable() {
					public void run()
					{
						makeGui();
					}
				});
	}

	public Benchmark(final JApplet pJApplet)
	{
		mTopLevelContainer = pJApplet;
		
		SwingUtilities.invokeLater(
				new Runnable() {
					public void run()
					{
						fillContentPane(pJApplet.getContentPane());
					}
				});
	}
	

	@Override
	public void actionPerformed(ActionEvent ae)
	{
		if(ae.getSource() == this.btnStart)
		{
			int width;
			int height;
			int runs;
			
			try
			{
				width = Integer.parseInt( this.txtMazeWidth.getText() );
				height = Integer.parseInt( this.txtMazeHeight.getText() );
				runs = Integer.parseInt( this.txtNumberOfRuns.getText() );
				
				if(width == 0 || height == 0 || runs == 0)
					throw new NumberFormatException();
				
				startBenchMark(width, height, runs);
			}
			catch(NumberFormatException ne)
			{
				JOptionPane.showMessageDialog(mTopLevelContainer, "Invalid width, height or runs!",
						"Input error", JOptionPane.ERROR_MESSAGE);
				
				return;
			}
		}
	}
	
	
	
	private void startBenchMark(int width, int height, int runs)
	{
		int maxSize = getProgressBarSize();
		
		//Initiate progressbar.
		progressBar.setMaximum(maxSize);
		progressBar.setValue(0);
		progressBar.setIndeterminate(false);
		
		showProgressBar(true);
		
		BenchmarkWorkerThread wt = new BenchmarkWorkerThread(width, height, runs);
		wt.addPropertyChangeListener(this);
		wt.execute();
	}
	
	private int getProgressBarSize()
	{
		int mazeTypes = 0;
		for(int i = 0; i < ckbxMazeType.length; i++)
		{
			if(ckbxMazeType[i].isSelected())
				mazeTypes++;
		}
		
		int builds = 0;
		for(int i = 0; i < ckbxBuild.length; i++)
		{
			if(ckbxBuild[i].isSelected())
				builds++;
		}
		
		int solves = 0;
		for(int i = 0; i < ckbxSolve.length; i++)
		{
			if(ckbxSolve[i].isSelected())
				solves++;
		}
		
		return (mazeTypes * builds * solves);
	}
	
	
	/**
	 * Way way way to much magic, but a nice exercise and it's only debug code anyway.
	 * Handle Single/Double array split.
	 */
	private String executeBenchmark(BenchmarkWorkerThread wt, int width, int height, int runs)
	{
		//Used in output
		StringBuilder sb = new StringBuilder();
		
		
		for(int i = 0; i < MazeGui.MazeType.values().length; i++)
		{
			//Only run if this box is selected.
			if(ckbxMazeType[i].isSelected())
			{
				try
				{
					//Retrieve the current maze type.
					MazeGui.MazeType mt = MazeGui.MazeType.values()[i];
					
					//Retrieve the current maze type class by using the java classloader.
					Class<?> mazeClass = Class.forName(
							this.getClass().getPackage().getName()+"."+mt.toString() );
					
					//Find the MazeType(int, int) constructor.
					Class<?>[] constrParams = {Integer.TYPE, Integer.TYPE};
					Constructor<?> constructor = mazeClass.getDeclaredConstructor(constrParams);
					
					//Execute the constructor of the Maze object.
					Maze maze = (Maze)constructor.newInstance(width, height);
					
					//Generate the firstpart of the log entry
					String logEntry = String.format(
							"X: %3d   Y: %3d   Runs: %2d   %-15s   ",
							width,
							height,
							runs,
							getShortName( maze.getClass() ));
					
					//Execute the build phase.
					executeBuildPhase(wt, sb, logEntry, maze, runs);
				}
				catch (ClassNotFoundException e) {
					e.printStackTrace();
				} catch (InstantiationException e) {
					e.printStackTrace();
				} catch (IllegalAccessException e) {
					e.printStackTrace();
				} catch (IllegalArgumentException e) {
					e.printStackTrace();
				} catch (InvocationTargetException e) {
					e.printStackTrace();
				} catch (SecurityException e) {
					e.printStackTrace();
				} catch (NoSuchMethodException e) {
					e.printStackTrace();
				}
			}
		}
		
		//Output the contents of the stringbuilder.
		sb.append('\n');
		return sb.toString();
	}
	
	/**
	 * Handle build algorithm split
	 */
	private void executeBuildPhase(BenchmarkWorkerThread wt, StringBuilder sb, String logEntry, Maze maze, int runs)
	{
		for(int i = 0; i < MazeGui.BuildAlgorithm.values().length; i++)
		{
			//Only run if this box is selected.
			if(ckbxBuild[i].isSelected())
			{
				try
				{
					//Retrieve the current build algorithm.
					MazeGui.BuildAlgorithm ba = MazeGui.BuildAlgorithm.values()[i];
					
					//Retrieve the current build class by using the java classloader.
					Class<?> buildClass = Class.forName(
							this.getClass().getPackage().getName()+".build."+ba.toString() );
					
					//Execute the default constructor of the Build object.
					Build build = (Build)buildClass.newInstance();
					
					//Execute and time build algorithm
					//Use 'runs' for the number of times.
					long buildTime;
					long startTime = System.currentTimeMillis();
					
					try
					{
						for(int j = 0; j < runs; j++)
						{
							maze.clear();
							build.initialise(maze);
							while(!build.isDone())
								build.tick();
						}
					}
					catch(BuildException be) {
						be.printStackTrace();
					}
					buildTime = System.currentTimeMillis() - startTime;
					
					//Make log entry part
					String logBuildpart = String.format(
							"%-19s (ms): %6d   ",
							getShortName( build.getClass() ),
							buildTime);
					
					//Execute the solve phase.
					executeSolvePhase(wt, sb, logEntry+logBuildpart, maze, runs);
				}
				catch (ClassNotFoundException e) {
					e.printStackTrace();
				} catch (InstantiationException e) {
					e.printStackTrace();
				} catch (IllegalAccessException e) {
					e.printStackTrace();
				} catch (IllegalArgumentException e) {
					e.printStackTrace();
				} catch (SecurityException e) {
					e.printStackTrace();
				}
			}
		}
	}
	
	/**
	 * Handle solve algorithm split
	 */
	private void executeSolvePhase(BenchmarkWorkerThread wt, StringBuilder sb, String logEntry, Maze maze, int runs)
	{
		for(int i = 0; i < MazeGui.SolveAlgorithm.values().length; i++)
		{
			//Only run if this box is selected.
			if(ckbxSolve[i].isSelected())
			{
				try
				{
					//Retrieve the current solve algorithm.
					MazeGui.SolveAlgorithm sa = MazeGui.SolveAlgorithm.values()[i];
					
					//Retrieve the current solve class by using the java classloader.
					Class<?> solveClass = Class.forName(
							this.getClass().getPackage().getName()+".solve."+sa.toString() );
					
					//Execute the default constructor of the Solve object.
					Solve solve = (Solve)solveClass.newInstance();
					
					
					//Execute and time solve algorithm
					//Use 'runs' for the number of times.
					long solveTime;
					long startTime = System.currentTimeMillis();
					try
					{
						for(int j = 0; j < runs; j++)
						{
							maze.clearSolveState();
							solve.initialise(maze);
							while(!solve.isDone())
								solve.tick();
						}
					}
					catch(SolveException se) {
						se.printStackTrace();
					}
					solveTime = System.currentTimeMillis() - startTime;
					
					//Make log entry part
					String logSolvepart = String.format(
							"%-23s (ms): %6d\n",
							getShortName( solve.getClass() ),
							solveTime);
					
					sb.append( logEntry );
					sb.append( logSolvepart );
					
					//Update progressbar
					wt.incrementProgress();
				}
				catch (ClassNotFoundException e) {
					e.printStackTrace();
				} catch (InstantiationException e) {
					e.printStackTrace();
				} catch (IllegalAccessException e) {
					e.printStackTrace();
				} catch (IllegalArgumentException e) {
					e.printStackTrace();
				} catch (SecurityException e) {
					e.printStackTrace();
				}
			}
		}
	}
	
	
	/**
	 * Retrieve the classname without package
	 */
	public static String getShortName(Class<?> c)
	{
		String className = c.getName();
		int index = className.lastIndexOf(".");
		if(index > -1) {
			return className.substring( index + 1, className.length() );
		}
		return className;
	}
	
	
	
	private void showProgressBar(boolean showProgress)
	{
		if(showProgress)
		{
			this.startAndProgressPanel.remove( this.btnStart );
			this.startAndProgressPanel.add( this.progressBar );
		}
		else
		{
			this.startAndProgressPanel.remove( this.progressBar );
			this.startAndProgressPanel.add( this.btnStart );
		}
		
		this.startAndProgressPanel.revalidate();
		this.startAndProgressPanel.repaint();
	}
	
	
	private void makeGui()
	{
		makeBenchmarkFrame();
		mTopLevelContainer.setVisible(true);
	}
	
	private void makeBenchmarkFrame()
	{
		JFrame lFrame = new JFrame();
		mTopLevelContainer = lFrame;
		lFrame.setTitle("AD2 Maze Benchmark");
		lFrame.setDefaultCloseOperation( JFrame.DISPOSE_ON_CLOSE );
		lFrame.setSize(FRAME_WIDTH, FRAME_HEIGHT);
		
		fillContentPane(lFrame.getContentPane());
		
		lFrame.setLocationRelativeTo(null);
	}
	
	private void fillContentPane(Container pContentPane)
	{
		pContentPane.setLayout(new BorderLayout());
		pContentPane.add( makeOptionsPanel(), BorderLayout.NORTH );
		pContentPane.add( makeTextAreaPanel(), BorderLayout.CENTER);
	}
	
	private JPanel makeOptionsPanel()
	{
		JPanel optionsPanel = new JPanel();
		
		optionsPanel.setLayout(new BoxLayout(optionsPanel, BoxLayout.LINE_AXIS));
		optionsPanel.setPreferredSize(new Dimension(0, OPTIONS_PANEL_HEIGHT));
		optionsPanel.add( makeGeneralOptionsPanel() );
		optionsPanel.add( makeCenterOptionsPanel() );
		optionsPanel.add( makeSolveOptionsPanel() );
		
		//These don't work or are pointless in a JApplet.
		if(mTopLevelContainer.getClass() == JFrame.class)
		{
			enableKeyBinds(optionsPanel);
		}
		
		return optionsPanel;
	}
	
	private JPanel makeGeneralOptionsPanel()
	{
		JPanel generalOptionsPanel = new JPanel();
		generalOptionsPanel.setBorder(BorderFactory.createTitledBorder("General Options"));
		
		GridBagLayout gridbag = new GridBagLayout();
		GridBagConstraints c = new GridBagConstraints();
		generalOptionsPanel.setLayout(gridbag);
		
		//Don't stretch Components & Align to the left
		c.fill = GridBagConstraints.NONE;
		c.anchor = GridBagConstraints.LINE_START;
		c.weightx = 1.0;
		c.weighty = 1.0;
		c.insets = new Insets(INSET_SIZE, INSET_SIZE, 0, 0);
		
		
		//Maze width
		c.gridx = 0;
		c.gridy = 0;
		generalOptionsPanel.add(new JLabel("Maze width:"), c);
		c.gridx = 1;
		txtMazeWidth = new JTextField( Integer.toString(DEFAULT_MAZE_X_SIZE) );
		txtMazeWidth.setColumns(6);
		txtMazeWidth.setHorizontalAlignment(JTextField.RIGHT);
		generalOptionsPanel.add(txtMazeWidth, c);
		
		//Maze height
		c.gridx = 0;
		c.gridy = 1;
		generalOptionsPanel.add(new JLabel("Maze height:"), c);
		c.gridx = 1;
		txtMazeHeight = new JTextField( Integer.toString(DEFAULT_MAZE_Y_SIZE) );
		txtMazeHeight.setColumns(6);
		txtMazeHeight.setHorizontalAlignment(JTextField.RIGHT);
		generalOptionsPanel.add(txtMazeHeight, c);
		
		//Number of runs
		c.gridx = 0;
		c.gridy = 2;
		generalOptionsPanel.add(new JLabel("Runs:"), c);
		c.gridx = 1;
		txtNumberOfRuns = new JTextField( Integer.toString(DEFAULT_RUNS) );
		txtNumberOfRuns.setColumns(6);
		txtNumberOfRuns.setHorizontalAlignment(JTextField.RIGHT);
		generalOptionsPanel.add(txtNumberOfRuns, c);
		
		//Single / Double maze type
		c.gridwidth = 2;
		c.gridx = 0;
		ckbxMazeType = new JCheckBox[ MazeGui.MazeType.values().length ];
		
		for(int i = 0; i < ckbxMazeType.length; i++)
		{
			ckbxMazeType[i] = new JCheckBox( MazeGui.MazeType.values()[i].toString() );
			ckbxMazeType[i].setSelected(true);
			c.gridy++;
			generalOptionsPanel.add(ckbxMazeType[i], c);
		}
		c.gridwidth = 1;
		
		return generalOptionsPanel;
	}
	
	private JPanel makeCenterOptionsPanel()
	{
		JPanel centerOptionsPanel = new JPanel();
		
		centerOptionsPanel.setLayout( new BoxLayout(centerOptionsPanel, BoxLayout.PAGE_AXIS ) );
		
		centerOptionsPanel.add( makeBuildOptionsPanel() );

		//Start panel
		startAndProgressPanel = new JPanel( new GridLayout(1,1) );
		startAndProgressPanel.setPreferredSize( new Dimension(0, START_PANEL_HEIGHT) );
		
		//Start button
		btnStart = new JButton("Start");
		btnStart.addActionListener(this);
		startAndProgressPanel.add(btnStart);
		
		//Progressbar (not visible in the beginning)
		progressBar = new JProgressBar();
		progressBar.setStringPainted(true);
		
		centerOptionsPanel.add( startAndProgressPanel );
		
		return centerOptionsPanel;
	}
	
	private JPanel makeBuildOptionsPanel()
	{
		JPanel buildOptionsPanel = new JPanel();
		buildOptionsPanel.setBorder(BorderFactory.createTitledBorder("Build Options"));

		GridBagLayout gridbag = new GridBagLayout();
		GridBagConstraints c = new GridBagConstraints();
		buildOptionsPanel.setLayout(gridbag);
		
		//Don't stretch Components & Align to the left
		c.fill = GridBagConstraints.NONE;
		c.anchor = GridBagConstraints.LINE_START;
		c.weightx = 1.0;
		c.weighty = 1.0;
		c.insets = new Insets(INSET_SIZE, INSET_SIZE, 0, 0);
		
		
		//Build algorithm check boxes
		ckbxBuild = new JCheckBox[ MazeGui.BuildAlgorithm.values().length ];
		
		c.gridy = -1;
		for(int i = 0; i < ckbxBuild.length; i++)
		{
			ckbxBuild[i] = new JCheckBox( MazeGui.BuildAlgorithm.values()[i].toString() );
			ckbxBuild[i].setSelected(true);
			c.gridy++;
			buildOptionsPanel.add(ckbxBuild[i], c);
		}
		
		return buildOptionsPanel;
	}
	
	private JPanel makeSolveOptionsPanel()
	{
		JPanel solveOptionsPanel = new JPanel();
		solveOptionsPanel.setBorder(BorderFactory.createTitledBorder("Solve Options"));
		
		GridBagLayout gridbag = new GridBagLayout();
		GridBagConstraints c = new GridBagConstraints();
		solveOptionsPanel.setLayout(gridbag);
		
		//Don't stretch Components & Align to the left
		c.fill = GridBagConstraints.NONE;
		c.anchor = GridBagConstraints.LINE_START;
		c.weightx = 1.0;
		c.weighty = 1.0;
		c.insets = new Insets(INSET_SIZE, INSET_SIZE, 0, 0);
		
		
		//Solve algorithm check boxes
		ckbxSolve = new JCheckBox[ MazeGui.SolveAlgorithm.values().length ];
		
		c.gridy = -1;
		for(int i = 0; i < ckbxSolve.length; i++)
		{
			ckbxSolve[i] = new JCheckBox( MazeGui.SolveAlgorithm.values()[i].toString() );
			ckbxSolve[i].setSelected(true);
			c.gridy++;
			solveOptionsPanel.add(ckbxSolve[i], c);
		}
		
		return solveOptionsPanel;
	}
	
	private JPanel makeTextAreaPanel()
	{
		JPanel textAreaPanel = new JPanel();
		textAreaPanel.setBorder(BorderFactory.createTitledBorder("Benchmark log"));
		textAreaPanel.setLayout( new GridLayout(1, 1) );

		txtLog = new JTextArea();
		txtLog.setFont( new Font("Monospaced", Font.PLAIN, 12) );
		
		JScrollPane scrollPane = new JScrollPane(txtLog);
		
		textAreaPanel.add(scrollPane);
		
		return textAreaPanel;
	}
	
	public void enableKeyBinds(JPanel generalMenuPanel)
	{
		//Maze Action object
		Action mazeAction = new AbstractAction(){
			private static final long serialVersionUID = 1L;

			public void actionPerformed(ActionEvent e)
			{
				((JFrame)mTopLevelContainer).dispose();
				SwingUtilities.invokeLater(new Main.GTInitGui());
			}
		};
		//Benchmark shortcut (Ctrl + B)
		KeyStroke mazeStroke = KeyStroke.getKeyStroke("ctrl B");
		generalMenuPanel.getInputMap(JPanel.WHEN_IN_FOCUSED_WINDOW).put(mazeStroke, "mazeAction");
		generalMenuPanel.getActionMap().put("mazeAction", mazeAction);
	}


	/**
	 * Used to update the progress.
	 */
	@Override
	public void propertyChange(PropertyChangeEvent evt) {
		
		if ("completed".equals( evt.getPropertyName() ))
		{
            int progress = (Integer) evt.getNewValue();
            progressBar.setValue(progress);
        }
	}
	
	/**
	 * SwingWorker thread to do the heavy lifting :p
	 */
	private class BenchmarkWorkerThread extends SwingWorker<Void, String>
	{
		private int width;
		private int height;
		private int runs;
		
		private int completed;
		private String output;
		
		public BenchmarkWorkerThread(int w, int h, int r)
		{
			this.width = w;
			this.height = h;
			this.runs = r;
			this.completed = 0;
		}
		
		/**
		 * Executed in a separate thread.
		 */
		@Override
		protected Void doInBackground() throws Exception
		{
			this.setProgress(0);
			
			this.output = executeBenchmark(this, this.width, this.height, this.runs);
			
			return null;
		}
		
		/**
		 * Executed in a separate thread.
		 * During benchmarking.
		 */
		public void incrementProgress()
		{
			this.completed++;
			this.firePropertyChange("completed", this.completed-1, this.completed);
		}
		
		/**
		 * Executed in EDT (Even Dispatch Thread).
		 */
		@Override
		protected void done()
		{
			Benchmark.this.showProgressBar(false);
			Benchmark.this.txtLog.append( this.output );
		}
	}
}
